package limit

import (
	"bytes"
	"chainmaker.org/chainmaker/smarteditor/conf"
	"chainmaker.org/chainmaker/smarteditor/contract"
	"chainmaker.org/chainmaker/smarteditor/logger"
	"errors"
	"io/fs"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"sync"
)

var log = logger.Get()

type UserResource struct {
	contractCount       int
	reachFileCountLimit bool
	fileCountMap        sync.Map //map[string]int
}

func (ur *UserResource) addFileCount(uid, path string, count int) {
	stat, err := os.Stat(path)
	if err != nil {
		log.Warnf("[addFileCount], read file info error. uid:%s, path:%s, count:%d", uid, path, count)
		return
	}
	//do not add excluded file to file limit
	if count != 0 && !stat.IsDir() && conf.IsExcludeFile(filepath.Ext(path)) {
		return
	}
	userContractKey := userContractKey(uid, path)
	value, ok := ur.fileCountMap.Load(userContractKey)
	if !ok {
		value = 0
	}
	ur.fileCountMap.Store(userContractKey, value.(int)+count)
}

func (ur *UserResource) addContract(uid, path string, count int) {
	ur.contractCount = ur.contractCount + count
	ur.addFileCount(uid, path, 0)
}

func (ur *UserResource) getFileCount(uid, path string) int {
	userContractKey := userContractKey(uid, path)
	value, ok := ur.fileCountMap.Load(userContractKey)
	if !ok {
		value = 0
	}
	i := value.(int)
	return i
}

func (ur *UserResource) String() string {
	buffer := bytes.Buffer{}
	buffer.WriteString("[")
	buffer.WriteString("contractCount: ")
	buffer.WriteString(strconv.Itoa(ur.contractCount))
	buffer.WriteString(", fileCount:[")
	valueSplit := ""
	ur.fileCountMap.Range(func(key, value interface{}) bool {
		buffer.WriteString(valueSplit)
		buffer.WriteString(key.(string))
		buffer.WriteString(":")
		buffer.WriteString(strconv.Itoa(value.(int)))
		valueSplit = ", "
		return true
	})
	buffer.WriteString("]")
	return buffer.String()
}

type UserResourcesManager struct {
	maxContractCount int
	maxFileCount     int
	userResourceMap  sync.Map //map[string]*UserResource
}

func NewUserResourcesManager(maxContractCount, maxFileCount int) *UserResourcesManager {
	return &UserResourcesManager{
		maxContractCount: maxContractCount,
		maxFileCount:     maxFileCount,
		userResourceMap:  sync.Map{},
	}
}

func (urm *UserResourcesManager) InitOrUpdateUserResourcesInfo(uid string) error {
	if oldResourceInfo, ok := urm.userResourceMap.Load(uid); ok {
		log.Debugf("[InitOrUpdateUserResourcesInfo] before init. resource:%v", oldResourceInfo)
	}

	resource := new(UserResource)
	resource.contractCount = 0
	resource.reachFileCountLimit = false
	urm.userResourceMap.Store(uid, resource)
	workspace := conf.GetUserWorkspace(uid)
	err := urm.initUserResourceInfo(uid, filepath.Join(workspace, conf.Src()), resource)
	if err != nil {
		resource.contractCount = urm.maxContractCount + 1
		resource.reachFileCountLimit = true
	}
	log.Debugf("[InitOrUpdateUserResourcesInfo] after init. resource:%s", resource)
	return err
}

func (urm *UserResourcesManager) initUserResourceInfo(uid, workspace string, resource *UserResource) error {
	err := filepath.WalkDir(workspace, func(path string, d fs.DirEntry, err error) error {
		//do nothing when path is root directory
		if workspace == path {
			return nil
		}

		if err != nil {
			return err
		}

		if d.IsDir() && contract.IsContractDirectory(uid, path) {
			resource.addContract(uid, path, 1)
		} else {
			resource.addFileCount(uid, path, 1)
		}
		return nil
	})
	return err
}

func (urm *UserResourcesManager) RemoveFile(uid, path string) error {
	value, ok := urm.userResourceMap.Load(uid)
	if !ok {
		log.Errorf("[UserResourcesManager] remove file, resource info not found. uid:%s, path:%s", uid, path)
		return errors.New("resource not found")
	}
	resource := value.(*UserResource)
	return filepath.WalkDir(path, func(path string, d fs.DirEntry, err error) error {
		if err != nil {
			return err
		}
		if d.IsDir() && contract.IsContractDirectory(uid, path) {
			resource.addContract(uid, path, -1)
		} else {
			resource.addFileCount(uid, path, -1)
		}
		return nil
	})
}

func (urm *UserResourcesManager) AddContract(uid, path string, count int) error {
	if value, ok := urm.userResourceMap.Load(uid); ok {
		resource := value.(*UserResource)
		resource.addContract(uid, path, count)
		return nil
	}
	return errors.New("user not found, uid: " + uid)
}

func (urm *UserResourcesManager) ReduceContract(uid string, count int) error {
	if value, ok := urm.userResourceMap.Load(uid); ok {
		resource := value.(*UserResource)
		resource.contractCount = resource.contractCount - count
		return nil
	}
	return errors.New("user not found, uid: " + uid)
}

func (urm *UserResourcesManager) AddFile(uid, path string, count int) error {
	if value, ok := urm.userResourceMap.Load(uid); ok {
		resource := value.(*UserResource)
		resource.addFileCount(uid, path, count)
		return nil
	}
	return errors.New("user not found, uid: " + uid)
}

func (urm *UserResourcesManager) ReduceFile(uid, path string, count int) error {
	if value, ok := urm.userResourceMap.Load(uid); ok {
		resource := value.(*UserResource)
		resource.addFileCount(uid, path, -1)
		return nil
	}
	return errors.New("user not found, uid: " + uid)
}

func (urm *UserResourcesManager) ReachContractCountLimit(uid string) bool {
	if value, ok := urm.userResourceMap.Load(uid); ok {
		resource := value.(*UserResource)
		return resource.contractCount >= urm.maxContractCount
	}
	return true
}

func (urm *UserResourcesManager) ReachFileCountLimit(uid, path string) bool {
	if value, ok := urm.userResourceMap.Load(uid); ok {
		resource := value.(*UserResource)
		return resource.reachFileCountLimit || resource.getFileCount(uid, path) >= urm.maxFileCount
	}
	return true
}

func (urm *UserResourcesManager) OverFileCountLimitAfterAdd(uid, path string, count int) bool {
	if value, ok := urm.userResourceMap.Load(uid); ok {
		resource := value.(*UserResource)
		return resource.reachFileCountLimit || resource.getFileCount(uid, path)+count > urm.maxFileCount
	}
	return true
}

func userContractKey(uid, path string) string {
	return uid + "_" + getContractNameFormPath(conf.GetUserWorkspace(uid), path)
}

func getContractNameFormPath(workspace, path string) string {
	if len(path) <= len(workspace) {
		return ""
	}

	path = filepath.FromSlash(path)
	srcPath := filepath.Join(workspace, conf.Src())
	prefixPathLength := len(srcPath + string(os.PathSeparator))
	if len(path) <= prefixPathLength {
		return ""
	}

	firstSplitIndex := strings.Index(path[prefixPathLength:], string(os.PathSeparator))
	if firstSplitIndex == -1 {
		return path[prefixPathLength:]
	}
	return path[prefixPathLength : prefixPathLength+firstSplitIndex]
}
